今天我們來認識一個相當常使用的 hook: useState。
const [currentValue, setCurrentValue] = useState(initialValue);
語法相當的簡單,其概念源自於解構賦值,我們把範例的 useState(0) 印出來看,可以看到 useState 回傳一個陣列,第一個參數是 state 的初始值,第二個名字叫 dispatchAction,意思可以想到是去修改 state 的函式。
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(pre => pre + 1)}>
Click me
</button>
</div>
);
}
注意: 更新 state 要使用 immutable 的寫法。
參考 為什麼更新 React 中的 state 要用 immutable 的寫法? 什麼是 immutable? 該如何寫才會是 immutable?
由於 react 有 state batch update 的特性,也就是在多次觸發同步事件去更新 state 時,會合併成一次的更新,元件只會重新渲染一次,減少了不必要的渲染。
根據此點特性,state 是透過 batching 去更新值,因此設定新的值給 state 後,馬上 console 印出來的值還是更新前的值。
以下範例中,點擊按鈕一下有三個 state 會更新,但只會有一次 re-render,這個就是 state batch update 的特性所導致。
state batch update 範例
那如果要馬上取得更新後的值怎麼做?
這篇 Andy Chang 大大寫的文章中有提到範例:
https://ithelp.ithome.com.tw/articles/10257994
這個還挺好用的,state 結合 ref,馬上就取得更新後的 state 值。
useStateRef(npm 網站)
這裡補充一段 React V18 新出的 API flushSync(),透過它可以解除 state 的 batch update,如有特殊的情境需求可以使用。
例如以下程式碼,在呼叫 api 後 Promise resolve 後呼叫兩個 setState 函式,不過透過 flushSync() 的作用,它們就不會進行批次渲染了。
const onFetchSomeData = () => {
axios.get(...).then((res) => {
ReactDOM.flushSync(() => {
setData(res.data); // 立刻重渲染
setFlag((f) => !f); // 立刻重渲染
});
});
}
另一個應用的例子是假如我們在 todolist 中新增一個 todo,功能是希望能夠滑到 todolist 的最底部,此時為了及時取到更新的 todo 才能順利的移到底部的話,就可以使用 flushSync。
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
兩者差異在 React 的新版文件說明得很清楚,前者因為 state batch update 的特性,多個 state 集中處理後再重新渲染,count 在該次 render 的值永遠都還是 0,所以最後加完還是 1,後者是取更新後的 state 繼續加,所以會出現 3。
export default function App() {
const [count1, setCount1] = useState(0);
useEffect(() => {
setCount1(count1 + 1);
setCount1(count1 + 1);
setCount1(count1 + 1);
}, []);
const [count2, setCount2] = useState(0);
useEffect(() => {
setCount2((prev) => prev + 1);
setCount2((prev) => prev + 1);
setCount2((prev) => prev + 1);
}, []);
return (
<div>
Current count1: {count1}
<!-- 1 -->
<br />
Current count2: {count2}
<!-- 3 -->
</div>
);
}
在更新物件、陣列型別的 state,也都是採用後者方式更新
setCount(prev => prev + 1)
背後的原理在上面的程式碼中,有段:
useEffect(() => {
setCount2((prev) => prev + 1);
setCount2((prev) => prev + 1);
setCount2((prev) => prev + 1);
}, []);
可以看到每個 setState 函式內都有一個函式 prev => prev + 1
,在 React 的底層運行中,這些函式都會被加入到 Queue 的資料結構中,所以你可以想像 Queue 裡面有三個函式等待執行,第一個函式執行完後,回傳的值會再丟給它下一個函式做為 prev 傳入去 + 1,最終結果就是 3。
再看看另一種情況: 假設初始 count = 0,點按鈕會結果會是?
<button onClick={() => {
setCount(n + 5);
setCount(n => n + 1);
}}>Increase the number</button>
這裡一樣 React 底層會設定 Queue 去儲存: [count + 5] => [(count + 5) => (count + 5) + 1]
,也是一樣的概念,第二個 setCount 取的函式參數為上一個 setCount 的回傳值,所以可以將 n 看作 count + 5
,所以最終結果就是 6。
讀者有沒有想過 state 要怎麼寫會比較好維護?比較好讀懂?
所以這裡就來補充一下撰寫 state 的幾個要注意的點:
例如我們將 x, y 合併成一個 state 代表 position,就不用兩個 state 了。
const [position, setPosition] = useState({ x: 0, y: 0 });
例如我們可以透過 firstName 和 lastName 去組成 fullName,所以就不用再多新增該 state。
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// 不必要: 多餘的 state 和 useEffect
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// 直接組成即可
const fullName = firstName + ' ' + lastName;
例如其實不需要多加一個 state 去儲存被選中的物品,可以改成用 id 去儲存。
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);
// 調整後
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
以下的範例也出現多餘的 state 和 useEffect,直接用一個變數儲存即可。
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 不必要
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// 較好的做法
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
}
初始化:const [someMap, setSomeMap] = useState(new Map());
更新:setSomeMap(new Map(someMap.set('someKey', 'a new value')));
redux 更新範例:
case 'SomeAction':
return {
...state,
yourMap: new Map(state.yourMap.set('someKey', 'a new value'))
}
這兩者的更新在 React 官網都寫蠻清楚的:
https://react.dev/learn/updating-objects-in-state
https://react.dev/learn/updating-arrays-in-state
const initialState = [
{id: 1, name: 'Alice', country: 'Austria'},
{id: 2, name: 'Bob', country: 'Belgium'},
];
const [employees, setEmployees] = useState(initialState);
// ✅ Add an object to a state array
const addObjectToArray = obj => {
setEmployees(current => [...current, obj]);
};
// ✅ Update one or more objects in a state array
const updateObjectInArray = () => {
setEmployees(current =>
current.map(obj => {
if (obj.id === 2) {
return {...obj, name: 'Sophia', country: 'Sweden'};
}
return obj;
}),
);
};
// ✅ Remove one or more objects from state array
const removeObjectFromArray = () => {
setEmployees(current =>
current.filter(obj => {
return obj.id !== 2;
}),
);
};
export default function Form() {
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
}
});
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
</>
);
}